﻿using System;
using System.Collections.Generic;
using System.Windows;
using System.Security;
using System.Security.Permissions;

namespace Pfz.Drawing.Wpf
{
    /// <summary>
    /// This class is returned by ManagedBitmap Lock and TryLock methods. This allows you to access pixels in a safe and relatively fast manner.
    /// </summary>
	[SecuritySafeCritical]
	[SuppressUnmanagedCodeSecurityAttribute]
    public sealed class LockedBitmap:
        IDisposable
    {
		#region Constructor and Destructor
			#region Constructor
				internal LockedBitmap(ManagedBitmap managedBitmap)
				{
					Bitmap = managedBitmap;

					var writeableBitmap = managedBitmap.WriteableBitmap;
					Width = writeableBitmap.PixelWidth;
					Height = writeableBitmap.PixelHeight;
					Stride = writeableBitmap.BackBufferStride;

					unsafe
					{
						Scanline0 = (byte *)writeableBitmap.BackBuffer.ToPointer();
					}
				}
			#endregion
			#region Dispose
				/// <summary>
				/// Unlocks the bitmap.
				/// </summary>
				public void Dispose()
				{
					var bitmap = Bitmap;
					if (bitmap != null)
					{
						Bitmap = null;

						bitmap.WriteableBitmap.Unlock();
					}
				}
			#endregion
		#endregion

		#region Properties
			#region this[]
				/// <summary>
				/// Gets or sets a pixel Argb value.
				/// </summary>
				public Argb this[int x, int y]
				{
					get
					{
						if (x < 0 || x >= Width)
							throw new ArgumentOutOfRangeException("x");

						if (y < 0 || y >= Height)
							throw new ArgumentOutOfRangeException("y");

						unsafe
						{
							byte *line = &Scanline0[y * Stride];
							Argb *typedLine = (Argb* )line;
							return typedLine[x];
						}
					}
					set
					{
						if (x < 0 || x >= Width)
							throw new ArgumentOutOfRangeException("x");

						if (y < 0 || y >= Height)
							throw new ArgumentOutOfRangeException("y");

						unsafe
						{
							byte* line = &Scanline0[y * Stride];
							Argb* typedLine = (Argb*)line;
							typedLine[x] = value;
						}
					}
				}
			#endregion
			#region Bitmap
				/// <summary>
				/// Gets the ManagedBitmap that created this LockedBitmap.
				/// </summary>
				public ManagedBitmap Bitmap { get; private set; }
			#endregion

			#region Width
				/// <summary>
				/// Gets the Width of the Bitmap.
				/// </summary>
				public int Width { get; private set; }
			#endregion
			#region Height
				/// <summary>
				/// Gets the Height of the Bitmap.
				/// </summary>
				public int Height { get; private set; }
			#endregion

			#region Stride
				/// <summary>
				/// Gets the Stride of the bitmap. (the number of bytes used for each row).
				/// </summary>
				public int Stride { get; private set; }
			#endregion
			#region Scanline0
				/// <summary>
				/// Gets the memory address of the first line.
				/// </summary>
				[CLSCompliant(false)]
				public unsafe byte *Scanline0 { get; private set; }
			#endregion
		#endregion
		#region Methods
			#region AddDirtyRect
				/// <summary>
				/// Tells the real bitmap that an area was changed, so controls using it as a BitmapSource can update themselves.
				/// </summary>
				public void AddDirtyRect(Int32Rect rect)
				{
					Bitmap.WriteableBitmap.AddDirtyRect(rect);
				}

				/// <summary>
				/// Tells the real bitmap that an area was changed, so controls using it as a BitmapSource can update themselves.
				/// </summary>
				public void AddDirtyRect(int x, int y, int width, int height)
				{
					AddDirtyRect(new Int32Rect(x, y, width, height));
				}
			#endregion

			#region DrawPoint
				/// <summary>
				/// Applies an Argb color to a given pixel, and allows combinations of transparency to be used.
				/// </summary>
				public void DrawPoint(int x, int y, Argb color, ColorCombineMode combineMode)
				{
					if (x < 0 || x >= Width)
						throw new ArgumentOutOfRangeException("x");

					if (y < 0 || y >= Height)
						throw new ArgumentOutOfRangeException("y");

					unsafe
					{
						byte *line = &Scanline0[y * Stride];
						Argb *typedLine = (Argb *)line;
						typedLine[x] = color.Combine(typedLine[x], combineMode);
					}
				}
			#endregion
			#region FillRectangle
                /// <summary>
                /// Replaces the color of a rectangle with the given color. Note that using a transparent color will replace the area with the transparent
                /// color, instead of applying it.
                /// </summary>
				public void FillRectangle(int left, int top, int width, int height, Argb color, ColorCombineMode combineMode)
				{
                    if (width == 0 || height == 0)
                        return;

                    if (width < 0)
                    {
                        left += width;
                        width = -width;
                    }

                    if (height < 0)
                    {
                        top += height;
                        height = -height;
                    }

                    if (left < 0)
					{
						width += left;
						left = 0;
					}

					if (top < 0)
					{
						height += top;
						top = 0;
					}

					int right = left + width;
					int diff = Width - right;
					if (diff < 0)
						width += diff;

					int bottom = top + height;
					diff = Height - bottom;
					if (diff < 0)
						height += diff;

					if (width <= 0 || height <= 0)
						return;

					unsafe
					{
						byte *lineStartBytes = &Scanline0[top * Stride + left * sizeof(Argb)];

                        switch (combineMode)
                        {
                            case ColorCombineMode.Replace:
						        for(int lineIndex=0; lineIndex<height; lineIndex++)
						        {
							        Argb *colorPointer = (Argb *)lineStartBytes;
							        for(int rowIndex=0; rowIndex<width; rowIndex++)
							        {
								        *colorPointer = color;
								        colorPointer++;
							        }

							        lineStartBytes += Stride;
						        }
                                break;

                            case ColorCombineMode.CombineIn:
						        for(int lineIndex=0; lineIndex<height; lineIndex++)
						        {
							        Argb *colorPointer = (Argb *)lineStartBytes;
							        for(int rowIndex=0; rowIndex<width; rowIndex++)
							        {
                                        Argb oldColor = *colorPointer;
                                        Argb newColor = color.CombineIn(oldColor);
								        *colorPointer = newColor;
								        colorPointer++;
							        }

							        lineStartBytes += Stride;
						        }
                                break;

                            case ColorCombineMode.CombineOver:
						        for(int lineIndex=0; lineIndex<height; lineIndex++)
						        {
							        Argb *colorPointer = (Argb *)lineStartBytes;
							        for(int rowIndex=0; rowIndex<width; rowIndex++)
							        {
                                        Argb oldColor = *colorPointer;
                                        Argb newColor = color.CombineOver(oldColor);
								        *colorPointer = newColor;
								        colorPointer++;
							        }

							        lineStartBytes += Stride;
						        }
                                break;

                            default:
                                throw new ArgumentException("Unknown value for combineMode.", "combineMode");
                        }
					}
				}
			#endregion
            #region Fill
                /// <summary>
                /// Fills an area, starting at the given point,  with the given color.
                /// </summary>
                public void Fill(int x, int y, Argb color, ColorCombineMode combineMode)
                {
                    Argb colorToReplace = this[x, y];
                    HashSet<KeyValuePair<int, int>> visitedPoints = new HashSet<KeyValuePair<int, int>>();
                    _Fill(x, y, colorToReplace, color, combineMode, visitedPoints);
                }
                private void _Fill(int x, int y, Argb colorToReplace, Argb color, ColorCombineMode combineMode, HashSet<KeyValuePair<int, int>> visitedPoints)
                {
                    if (x < 0 || y < 0 || x >= Width || y >= Height)
                        return;

                    var point = new KeyValuePair<int, int>(x, y);
                    if (!visitedPoints.Add(point))
                        return;

                    if (this[x, y] == colorToReplace)
                    {
                        DrawPoint(x, y, color, combineMode);
                        _Fill(x - 1, y - 1, colorToReplace, color, combineMode, visitedPoints);
                        _Fill(x   ,  y - 1, colorToReplace, color, combineMode, visitedPoints);
                        _Fill(x + 1, y - 1, colorToReplace, color, combineMode, visitedPoints);

                        _Fill(x - 1, y, colorToReplace, color, combineMode, visitedPoints);
                        _Fill(x    , y, colorToReplace, color, combineMode, visitedPoints);
                        _Fill(x + 1, y, colorToReplace, color, combineMode, visitedPoints);

                        _Fill(x - 1, y + 1, colorToReplace, color, combineMode, visitedPoints);
                        _Fill(x    , y + 1, colorToReplace, color, combineMode, visitedPoints);
                        _Fill(x + 1, y + 1, colorToReplace, color, combineMode, visitedPoints);
                    }
                }
            #endregion

			#region DrawBlockTo
				/// <summary>
				/// Applies the pixels of this bitmap to another one, so transparent colors will be combined.
				/// </summary>
				public void DrawBlockTo(LockedBitmap to, ColorCombineMode combineMode)
				{
					if (to == null)
						throw new ArgumentNullException("to");

					int minWidth = Math.Min(Width, to.Width);
					int minHeight = Math.Min(Height, to.Height);

					_DrawBlockTo(to, combineMode, 0, 0, 0, 0, minWidth, minHeight);
				}

				/// <summary>
				/// Applies the pixels of this bitmap to another one, so transparent colors will be combined.
				/// </summary>
				public void DrawBlockTo(LockedBitmap to, ColorCombineMode combineMode, int sourceLeft, int sourceTop, int destinationLeft, int destinationTop, int width, int height)
				{
					if (to == null)
						throw new ArgumentNullException("to");

					if (width <= 0 || height <= 0)
						return;

					int minLeft = Math.Min(sourceLeft, destinationLeft);
					if (minLeft < 0)
					{
						width += minLeft;
						sourceLeft -= minLeft;
						destinationLeft -= minLeft;
					}

					int minTop = Math.Min(sourceTop, destinationTop);
					if (minTop < 0)
					{
						height += minTop;
						sourceTop -= minTop;
						destinationTop -= minTop;
					}

					int sourceRight = sourceLeft + width;
					int destRight = destinationLeft + width;
					int diffRight = Math.Min(Width - sourceRight, to.Width - destRight);
					if (diffRight < 0)
						width += diffRight;

					int sourceBottom = sourceTop + height;
					int destBottom = destinationTop + height;
					int diffBottom = Math.Min(Height - sourceBottom, to.Height - destBottom);
					if (diffBottom < 0)
						height += diffBottom;

					if (width <= 0 || height <= 0)
						return;

					_DrawBlockTo(to, combineMode, sourceLeft, sourceTop, destinationLeft, destinationTop, width, height);

				}
				private void _DrawBlockTo(LockedBitmap to, ColorCombineMode combineMode, int sourceLeft, int sourceTop, int destinationLeft, int destinationTop, int width, int height)
				{
					unsafe
					{
						int sourceStride = Stride;
						int destStride = to.Stride;

						byte* sourceBytes = &Scanline0[sourceTop * sourceStride + sourceLeft * sizeof(Argb)];
						byte* destBytes = &to.Scanline0[destinationTop * destStride + destinationLeft * sizeof(Argb)];

                        switch (combineMode)
                        {
                            case ColorCombineMode.Replace:
						        for (int y = 0; y < height; y++)
						        {
							        Argb* source = (Argb*)sourceBytes;
							        Argb* dest = (Argb*)destBytes;

							        for (int x = 0; x < width; x++)
								        *dest++ = *source++;

							        sourceBytes += sourceStride;
							        destBytes += destStride;
						        }
                                break;

                            case ColorCombineMode.CombineIn:
						        for (int y = 0; y < height; y++)
						        {
							        Argb* source = (Argb*)sourceBytes;
							        Argb* dest = (Argb*)destBytes;

							        for (int x = 0; x < width; x++)
							        {
								        Argb sourceColor = *source++;
								        Argb destColor = *dest;

								        *dest++ = sourceColor.CombineIn(destColor);
							        }

							        sourceBytes += sourceStride;
							        destBytes += destStride;
						        }
                                break;

                            case ColorCombineMode.CombineOver:
						        for (int y = 0; y < height; y++)
						        {
							        Argb* source = (Argb*)sourceBytes;
							        Argb* dest = (Argb*)destBytes;

							        for (int x = 0; x < width; x++)
							        {
								        Argb sourceColor = *source++;
								        Argb destColor = *dest;

								        *dest++ = sourceColor.CombineOver(destColor);
							        }

							        sourceBytes += sourceStride;
							        destBytes += destStride;
						        }
                                break;

                            default:
                                throw new ArgumentException("Unknown value for combineMode.", "combineMode");
                        }
					}
				}
            #endregion
        #endregion
    }
}
